/**
 * Copyright (c) 2008 Trent Hoeppner
 * 
 * This file is part of BillCalc.
 * 
 * BillCalc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * BillCalc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with BillCalc.  If not, see <http://www.gnu.org/licenses/>.
 **/

package com.trentech.billcalc;

import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import testcases.MainTest;

/**
 * DateGroup represents a series of calendar dates.  The dates need not all be
 * contiguous, and allows for open-ended ranges.
 *  
 * @author Trent Hoeppner
 */
public class DateGroup {
    
    /**
     * The number of milliseconds in one day.
     */
	// original : 
    // public static final int DAY = 24 * 60 * 60 * 1000;
	// seed fault : 
	public static final int DAY = (MainTest.isOriginal   ? 24 : 20) * 60 * 60 * 1000;
    

    /**
     * The series of DateRanges that make this Group.  These are ordered from
     * oldest to newest.
     */
    private List<DateRange> ranges = new ArrayList<DateRange>();
    
    
    /**
     * The last index in {@link #ranges} where a {@link #startRange(Date)} was
     * performed.  This will be -1 if all starts have been followed by an end.
     */
    private int lastStartRangeIndex = -1;
    
    
    /**
     * Constructor for DateGroup.
     */
    public DateGroup() {
        // do nothing
    }
    
    
    /**
     * Starts a new range of dates.
     * 
     * @param   startDate   the date to start the range at.  Cannot be null.
     */
    public void startRange(Date startDate) {
        int rangeIndex = rangeIndexOf(startDate);
        
        // original : 
        // if (rangeIndex < 0) { 
        // seed fault :
        if (MainTest.isOriginal || MainTest.isFaultFound ? rangeIndex < 0 : rangeIndex > 0) {
        	
            DateRange range = new DateRange(startDate, null);
            rangeIndex = ranges.size();
            ranges.add(range);
        }
        
        lastStartRangeIndex = rangeIndex;
    }
    
    
    /**
     * Ends a range of dates that has already started.  
     * {@link #startRange(Date)} must be called each time before this method is 
     * called.
     * 
     * @param   endDate the date to end the range at.  Must be on or after the
     *                  start date.
     */
    public void endRange(Date endDate) {
        if (lastStartRangeIndex < 0) {
            throw new IllegalStateException("Cannot create an end if there is no beginning.");
        }
        
        DateRange range = ranges.get(lastStartRangeIndex);
        
        // original :
        // if (range.isOpenEnded() || !range.contains(endDate)) {
        // seed fault :
        if (MainTest.isOriginal || MainTest.isFaultFound ? (range.isOpenEnded() || !range.contains(endDate)) : (range.isOpenEnded() && !range.contains(endDate))) {
        	
            range = range.extend(endDate);
            ranges.set(lastStartRangeIndex, range);
        }
        
        lastStartRangeIndex = -1;
    }
    
    
    /**
     * Returns whether the given day is included in this.
     * 
     * @param   testDay the day to check for inclusion.  Cannot be null.
     * 
     * @return  true if the given day is in this, false otherwise.
     */
    public boolean contains(Date testDay) {
        boolean isInRange = rangeIndexOf(testDay) >= 0;
        return isInRange;
    }
    
    
    /**
     * Returns the first date of the first series of dates.
     * 
     * @return  the first date in this.  Will not be null.
     */
    public Date getFirstDate() {
        return ranges.get(0).startDate;
    }
    
    
    /**
     * Returns the last date of the last series of dates.
     * 
     * @return  the last date in this.  May be null if the last series is 
     *          open-ended.
     */
    public Date getLastDate() {
        return ranges.get(ranges.size() - 1).endDate;
    }
    
    
    /**
     * Returns the index in {@link #ranges} where the given day occurs.
     * 
     * @param   date    the day to try to find the index of.  Cannot be null.
     * 
     * @return  the index in {@link #ranges} where the given day occurs, or
     *          -1 if a range could not be found.
     */
    private int rangeIndexOf(Date date) {
        int foundIndex = -1;
        Iterator<DateRange> rangeIterator = ranges.iterator();
        for (int rangeIndex = 0; rangeIterator.hasNext(); rangeIndex++) {
            DateRange range = rangeIterator.next();
            if (range.contains(date)) {
                foundIndex = rangeIndex;
                break;
            }
        }
        
        return foundIndex;
    }
    
    
    /**
     * Calculates the endDay given the number of days after a startDay.  This
     * operation is the inverse of {@link #calcDays}.
     * 
     * @param   startDay        the day to start from (inclusive).  Cannot be 
     *                          null.
     * @param   numDaysAfter    the number of days after and including the 
     *                          startDay.  Must be >= 0.
     *                          
     * @return  the calculated end date (exclusive).  Will not be null.
     */
    private Date calcDateFromStart(Date startDay, int numDaysAfter) {
        long daysInMillis = ((long) numDaysAfter) * DAY;
        Date date = new Date(startDay.getTime() + daysInMillis);
        return date;
    }
    
    
    /**
     * DateRange is range of dates, with an optional beginning and end.  This
     * class is immutable.
     */
    private class DateRange {
        
        /**
         * The start of the range.  May be null if there is no beginning.
         */
        private Date startDate;
        
        
        /**
         * The end of the range.  Must be the same or after {@link #startDate}.
         * May be null if there is no end.
         */
        private Date endDate;
        
        
        /**
         * Constructor for DateRange.
         * 
         * @param   startDate   the start of the range, or null to indicate 
         *                      that there is no start.
         * @param   endDate     the end of the range, or null to indicate that
         *                      there is no end.  If not null, must be the same 
         *                      or after startDate.
         */
        public DateRange(Date startDate, Date endDate) {
            this.startDate = startDate;
            this.endDate = endDate;
        }
        
        
        /**
         * Returns a new DateRange with the given end date.
         * 
         * @param   newEndDate  the new end date to set for the returned range.
         * 
         * @return  a new DateRange with the start date of this and the given 
         *          end date.  Will not be null.
         */
        public DateRange extend(Date newEndDate) {
            return new DateRange(startDate, newEndDate);
        }
        
        
        /**
         * Returns whether this has an end date.
         * 
         * @return  true if this has no end date, false otherwise.
         */
        public boolean isOpenEnded() {
            return endDate == null;
        }
        
        
        /**
         * Returns true if the given testDay is between startDay (inclusive) and 
         * endDay (exclusive).
         * 
         * @param   testDay the day to check the bounds for.  Cannot be null.
         * 
         * @return  true if testDay falls in the range, false otherwise.
         */
        private boolean contains(Date testDay) {
            boolean startPassed;
            if (startDate == null) {
                // there is no lower bound
                startPassed = true;
            }
            else {
                startPassed = testDay.equals(startDate) || testDay.after(startDate); 
            }
            
            boolean endPassed;
            if (endDate == null) {
                // there is no upper bound
                endPassed = true;
            }
            else {
                endPassed = testDay.before(calcDateFromStart(endDate, 1)); 
            }
            
            return startPassed && endPassed;
        }
    }
}
